﻿/*
 
Celerity: Sensory Overload
Copright 2012 by Adam Hill, David Gannon, Thomas Tamblyn & Patrick Ytting
Contact: adam.hill@outlook.com
Website: adamhill.org
Licence: MS-Pl Licence
  
 */

using Celerity.Data;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Linq;
using System.Threading.Tasks;
using DR = System.Drawing;
//using WinForms = System.Windows.Forms;

namespace Celerity.CV
{
    public sealed class CVModule : IDisposable
    {
        #region Custom Events
        public event EventHandler WebcamStatusChanged;
        void RaiseWebcamStatusChanged()
        {
            if (WebcamStatusChanged != null)
            {
                WebcamStatusChanged(this, new EventArgs());
            }
        }
        #endregion /Custom Events

        #region Constants
        const int minNeighbours = 1; // Drastically affects CV performance (don't go higher than 4)
        const double scaleFactor = 1.4;
        const string haarDataSource = @"Content\Data\haarcascade_frontalface_alt2.xml";
        #endregion /Constants

        #region Instance Variables
        // Logic
        bool isFirstFaceCapture = true;
        int msSinceLastWebcamReadyTest = 0;

        // Capture & Haar
        Capture cap;
        HaarCascade haar;
        Image<Gray, byte> grayframe;
        DR.Size minFaceSize;

        // Tracking
        int lastX;
        int lastY;
        int lastWidth;
        int lastHeight;

        // Smoothing
        const float smoothingN = 20f;
        const float alphaEMA = 2f / (smoothingN + 1f);
        const float inverseAlphaEMA = 1f - alphaEMA;
        Rectangle previousEMA = new Rectangle(0, 0, 0, 0);
        Rectangle currentEMA = new Rectangle(0, 0, 0, 0);
        #endregion /Instance Variables

        #region Public Variables
        public int FramesCaptured;
        public bool IsFaceDetected;
        public bool IsWebcamWorking;
        public Texture2D LastCameraImage;
        public Ellipse FaceEllipse;
        public Vector3 HeadPos;

        // Stats which indicate where and how big the Face is 
        // relative to the webcam view
        // which will be used to calculate the viewing angle etc.
        public float FaceCentrePercentX = 0f;
        public float FaceCentrePercentY = 0f;
        public float FaceSizePercentWidth = 0f;
        public float FaceSizePercentHeight = 0f;
        #endregion /Public Variables

        #region Constructor
        public CVModule()
        {
            HeadPos = Vector3.Zero;
            haar = new HaarCascade(haarDataSource);
            TryLocateWebcam();
        }
        #endregion /Constructor

        public void Update(int elapsedMs)
        {
            msSinceLastWebcamReadyTest += elapsedMs;

            if (cap == null)
            {
                IsWebcamWorking = false;
            }

            if (IsWebcamWorking)
            {
                Parallel.Invoke(() => QueryCamera());
            }
            else
            {
                if (msSinceLastWebcamReadyTest >= CeleritySettings.CameraTestDelayMs)
                {
                    TryLocateWebcam();
                    msSinceLastWebcamReadyTest = 0;
                }
            }
        }

        void TryLocateWebcam()
        {
            try
            {
                if (cap == null)
                {
                    // Get first camera
                    cap = new Capture();
                    cap.SetCaptureProperty(Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FRAME_WIDTH, 320);
                    cap.SetCaptureProperty(Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FRAME_HEIGHT, 240);
                    cap.SetCaptureProperty(Emgu.CV.CvEnum.CAP_PROP.CV_CAP_PROP_FPS, 30);
                    if (!IsWebcamWorking)
                    {
                        IsWebcamWorking = true;
                        GlobalGameStates.ControlAvailableWebcam = true;
                        RaiseWebcamStatusChanged();
                    }
                }
                else
                {
                    IsWebcamWorking = true;
                }
            }
            catch
            {
                if (IsWebcamWorking)
                {
                    RaiseWebcamStatusChanged();
                }

                IsWebcamWorking = false;
            }
        }

        void QueryCamera()
        {
            try
            {
                if (IsWebcamWorking)
                {
                    if (GlobalGameStates.CameraState != CameraState.Override)
                    {
                        grayframe = cap.QueryGrayFrame();
                        if (grayframe != null)
                        {
                            FramesCaptured++;
                            DetectFaces();
                        }
                    }
                }
                else
                {
                    TryLocateWebcam();
                }
            }
            catch
            {
                IsWebcamWorking = false;
            }
        }

        void DetectFaces()
        {
            if (minFaceSize == null || minFaceSize.IsEmpty)
            {
                minFaceSize = new DR.Size(grayframe.Width / 8, grayframe.Height / 8);
            }

            // There's only one channel (greyscale), hence the zero index
            var faces = grayframe.DetectHaarCascade(
                            haar,
                            scaleFactor,
                            minNeighbours,
                            HAAR_DETECTION_TYPE.DO_ROUGH_SEARCH,
                            minFaceSize
                            )[0];

            IsFaceDetected = faces.Any();

            if (IsFaceDetected)
            {
                foreach (var face in faces)
                {
                    if (isFirstFaceCapture)
                    {
                        // If first time then set entire history to current frame
                        isFirstFaceCapture = false;
                        currentEMA.Width = face.rect.Width;
                        currentEMA.Height = face.rect.Height;
                        previousEMA.Width = face.rect.Width;
                        previousEMA.Height = face.rect.Height;
                    }

                    lastX = face.rect.X;
                    lastY = face.rect.Y;
                    lastWidth = face.rect.Width;
                    lastHeight = face.rect.Height;

                    // New smoothing stuff
                    currentEMA.Width = (int)(alphaEMA * lastWidth + inverseAlphaEMA * previousEMA.Width);
                    currentEMA.Height = (int)(alphaEMA * lastHeight + inverseAlphaEMA * previousEMA.Height);
                    previousEMA.Width = currentEMA.Width;
                    previousEMA.Height = currentEMA.Height;
                }
            }

            // Draw an ellipse round the face
            DR.PointF ellipseCenterPoint = new DR.PointF(lastX + lastWidth / 2.0f, lastY + lastHeight / 2.0f);
            DR.SizeF ellipseSize = new DR.SizeF(currentEMA.Width, currentEMA.Height);
            FaceEllipse = new Ellipse(ellipseCenterPoint, ellipseSize, 0);

            // Public stats
            FaceCentrePercentX = 1 - ellipseCenterPoint.X / (float)grayframe.Width;
            FaceCentrePercentY = ellipseCenterPoint.Y / (float)grayframe.Width;
            FaceSizePercentWidth = ellipseSize.Width / (float)grayframe.Width;
            FaceSizePercentHeight = ellipseSize.Height / (float)grayframe.Height;

            // Head Pos for feeding into world camera (range of -1f to 1f)
            HeadPos.X = -1f + (2f * FaceCentrePercentX);
            HeadPos.Y = -1f + (2f * FaceCentrePercentY);
            HeadPos.Z = -1f + (2f * FaceSizePercentHeight);
        }

        public void Dispose()
        {
            cap.Dispose();
            haar.Dispose();
            this.Dispose();
            GC.SuppressFinalize(this);
        }
    }
}
